"""Weather component that handles meteorological data for your location."""

from __future__ import annotations

import abc
from collections.abc import Callable, Iterable
from contextlib import suppress
from datetime import timedelta
from functools import partial
import logging
from typing import (
    Any,
    Final,
    Generic,
    Literal,
    Required,
    TypedDict,
    TypeVar,
    cast,
    final,
)

from propcache.api import cached_property
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
    PRECISION_HALVES,
    PRECISION_TENTHS,
    PRECISION_WHOLE,
    UnitOfPressure,
    UnitOfSpeed,
    UnitOfTemperature,
)
from homeassistant.core import (
    CALLBACK_TYPE,
    HomeAssistant,
    ServiceCall,
    ServiceResponse,
    SupportsResponse,
    callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import ABCCachedProperties, Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import (
    CoordinatorEntity,
    DataUpdateCoordinator,
    TimestampDataUpdateCoordinator,
)
from homeassistant.util.dt import utcnow
from homeassistant.util.json import JsonValueType
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM

from .const import (  # noqa: F401
    ATTR_WEATHER_APPARENT_TEMPERATURE,
    ATTR_WEATHER_CLOUD_COVERAGE,
    ATTR_WEATHER_DEW_POINT,
    ATTR_WEATHER_HUMIDITY,
    ATTR_WEATHER_OZONE,
    ATTR_WEATHER_PRECIPITATION_UNIT,
    ATTR_WEATHER_PRESSURE,
    ATTR_WEATHER_PRESSURE_UNIT,
    ATTR_WEATHER_TEMPERATURE,
    ATTR_WEATHER_TEMPERATURE_UNIT,
    ATTR_WEATHER_UV_INDEX,
    ATTR_WEATHER_VISIBILITY,
    ATTR_WEATHER_VISIBILITY_UNIT,
    ATTR_WEATHER_WIND_BEARING,
    ATTR_WEATHER_WIND_GUST_SPEED,
    ATTR_WEATHER_WIND_SPEED,
    ATTR_WEATHER_WIND_SPEED_UNIT,
    DATA_COMPONENT,
    DOMAIN,
    INTENT_GET_WEATHER,
    UNIT_CONVERSIONS,
    VALID_UNITS,
    WeatherEntityFeature,
)
from .websocket_api import async_setup as async_setup_ws_api

_LOGGER = logging.getLogger(__name__)

ENTITY_ID_FORMAT = DOMAIN + ".{}"
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
SCAN_INTERVAL = timedelta(seconds=30)

ATTR_CONDITION_CLASS = "condition_class"
ATTR_CONDITION_CLEAR_NIGHT = "clear-night"
ATTR_CONDITION_CLOUDY = "cloudy"
ATTR_CONDITION_EXCEPTIONAL = "exceptional"
ATTR_CONDITION_FOG = "fog"
ATTR_CONDITION_HAIL = "hail"
ATTR_CONDITION_LIGHTNING = "lightning"
ATTR_CONDITION_LIGHTNING_RAINY = "lightning-rainy"
ATTR_CONDITION_PARTLYCLOUDY = "partlycloudy"
ATTR_CONDITION_POURING = "pouring"
ATTR_CONDITION_RAINY = "rainy"
ATTR_CONDITION_SNOWY = "snowy"
ATTR_CONDITION_SNOWY_RAINY = "snowy-rainy"
ATTR_CONDITION_SUNNY = "sunny"
ATTR_CONDITION_WINDY = "windy"
ATTR_CONDITION_WINDY_VARIANT = "windy-variant"
ATTR_FORECAST_IS_DAYTIME: Final = "is_daytime"
ATTR_FORECAST_CONDITION: Final = "condition"
ATTR_FORECAST_HUMIDITY: Final = "humidity"
ATTR_FORECAST_NATIVE_PRECIPITATION: Final = "native_precipitation"
ATTR_FORECAST_PRECIPITATION: Final = "precipitation"
ATTR_FORECAST_PRECIPITATION_PROBABILITY: Final = "precipitation_probability"
ATTR_FORECAST_NATIVE_PRESSURE: Final = "native_pressure"
ATTR_FORECAST_PRESSURE: Final = "pressure"
ATTR_FORECAST_NATIVE_APPARENT_TEMP: Final = "native_apparent_temperature"
ATTR_FORECAST_APPARENT_TEMP: Final = "apparent_temperature"
ATTR_FORECAST_NATIVE_TEMP: Final = "native_temperature"
ATTR_FORECAST_TEMP: Final = "temperature"
ATTR_FORECAST_NATIVE_TEMP_LOW: Final = "native_templow"
ATTR_FORECAST_TEMP_LOW: Final = "templow"
ATTR_FORECAST_TIME: Final = "datetime"
ATTR_FORECAST_WIND_BEARING: Final = "wind_bearing"
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: Final = "native_wind_gust_speed"
ATTR_FORECAST_WIND_GUST_SPEED: Final = "wind_gust_speed"
ATTR_FORECAST_NATIVE_WIND_SPEED: Final = "native_wind_speed"
ATTR_FORECAST_WIND_SPEED: Final = "wind_speed"
ATTR_FORECAST_NATIVE_DEW_POINT: Final = "native_dew_point"
ATTR_FORECAST_DEW_POINT: Final = "dew_point"
ATTR_FORECAST_CLOUD_COVERAGE: Final = "cloud_coverage"
ATTR_FORECAST_UV_INDEX: Final = "uv_index"

ROUNDING_PRECISION = 2

SERVICE_GET_FORECASTS: Final = "get_forecasts"

_ObservationUpdateCoordinatorT = TypeVar(
    "_ObservationUpdateCoordinatorT",
    bound=DataUpdateCoordinator[Any],
    default=DataUpdateCoordinator[dict[str, Any]],
)

_DailyForecastUpdateCoordinatorT = TypeVar(
    "_DailyForecastUpdateCoordinatorT",
    bound=TimestampDataUpdateCoordinator[Any],
    default=TimestampDataUpdateCoordinator[None],
)
_HourlyForecastUpdateCoordinatorT = TypeVar(
    "_HourlyForecastUpdateCoordinatorT",
    bound=TimestampDataUpdateCoordinator[Any],
    default=_DailyForecastUpdateCoordinatorT,
)
_TwiceDailyForecastUpdateCoordinatorT = TypeVar(
    "_TwiceDailyForecastUpdateCoordinatorT",
    bound=TimestampDataUpdateCoordinator[Any],
    default=_DailyForecastUpdateCoordinatorT,
)

# mypy: disallow-any-generics


def round_temperature(temperature: float | None, precision: float) -> float | None:
    """Convert temperature into preferred precision for display."""
    if temperature is None:
        return None

    # Round in the units appropriate
    if precision == PRECISION_HALVES:
        temperature = round(temperature * 2) / 2.0
    elif precision == PRECISION_TENTHS:
        temperature = round(temperature, 1)
    # Integer as a fall back (PRECISION_WHOLE)
    else:
        temperature = round(temperature)

    return temperature


class Forecast(TypedDict, total=False):
    """Typed weather forecast dict.

    All attributes are in native units and old attributes kept
    for backwards compatibility.
    """

    condition: str | None
    datetime: Required[str]
    humidity: float | None
    precipitation_probability: int | None
    cloud_coverage: int | None
    native_precipitation: float | None
    precipitation: None
    native_pressure: float | None
    pressure: None
    native_temperature: float | None
    temperature: None
    native_templow: float | None
    templow: None
    native_apparent_temperature: float | None
    wind_bearing: float | str | None
    native_wind_gust_speed: float | None
    native_wind_speed: float | None
    wind_speed: None
    native_dew_point: float | None
    uv_index: float | None
    is_daytime: bool | None  # Mandatory to use with forecast_twice_daily


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up the weather component."""
    component = hass.data[DATA_COMPONENT] = EntityComponent[WeatherEntity](
        _LOGGER, DOMAIN, hass, SCAN_INTERVAL
    )
    component.async_register_entity_service(
        SERVICE_GET_FORECASTS,
        {vol.Required("type"): vol.In(("daily", "hourly", "twice_daily"))},
        async_get_forecasts_service,
        required_features=[
            WeatherEntityFeature.FORECAST_DAILY,
            WeatherEntityFeature.FORECAST_HOURLY,
            WeatherEntityFeature.FORECAST_TWICE_DAILY,
        ],
        supports_response=SupportsResponse.ONLY,
    )
    async_setup_ws_api(hass)
    await component.async_setup(config)
    return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up a config entry."""
    return await hass.data[DATA_COMPONENT].async_setup_entry(entry)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Unload a config entry."""
    return await hass.data[DATA_COMPONENT].async_unload_entry(entry)


class WeatherEntityDescription(EntityDescription, frozen_or_thawed=True):
    """A class that describes weather entities."""


class PostInitMeta(ABCCachedProperties):
    """Meta class which calls __post_init__ after __new__ and __init__."""

    def __call__(cls, *args: Any, **kwargs: Any) -> Any:  # noqa: N805  ruff bug, ruff does not understand this is a metaclass
        """Create an instance."""
        instance: PostInit = super().__call__(*args, **kwargs)
        instance.__post_init__(*args, **kwargs)
        return instance


class PostInit(metaclass=PostInitMeta):
    """Class which calls __post_init__ after __new__ and __init__."""

    @abc.abstractmethod
    def __post_init__(self, *args: Any, **kwargs: Any) -> None:
        """Finish initializing."""


CACHED_PROPERTIES_WITH_ATTR_ = {
    "native_apparent_temperature",
    "native_temperature",
    "native_temperature_unit",
    "native_dew_point",
    "native_pressure",
    "native_pressure_unit",
    "humidity",
    "native_wind_gust_speed",
    "native_wind_speed",
    "native_wind_speed_unit",
    "wind_bearing",
    "ozone",
    "cloud_coverage",
    "uv_index",
    "native_visibility",
    "native_visibility_unit",
    "native_precipitation_unit",
    "condition",
}


class WeatherEntity(Entity, PostInit, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
    """ABC for weather data."""

    entity_description: WeatherEntityDescription
    _attr_condition: str | None = None
    _attr_humidity: float | None = None
    _attr_ozone: float | None = None
    _attr_cloud_coverage: int | None = None
    _attr_uv_index: float | None = None
    _attr_precision: float
    _attr_state: None = None
    _attr_wind_bearing: float | str | None = None

    _attr_native_pressure: float | None = None
    _attr_native_pressure_unit: str | None = None
    _attr_native_apparent_temperature: float | None = None
    _attr_native_temperature: float | None = None
    _attr_native_temperature_unit: str | None = None
    _attr_native_visibility: float | None = None
    _attr_native_visibility_unit: str | None = None
    _attr_native_precipitation_unit: str | None = None
    _attr_native_wind_gust_speed: float | None = None
    _attr_native_wind_speed: float | None = None
    _attr_native_wind_speed_unit: str | None = None
    _attr_native_dew_point: float | None = None

    _forecast_listeners: dict[
        Literal["daily", "hourly", "twice_daily"],
        list[Callable[[list[JsonValueType] | None], None]],
    ]

    _weather_option_temperature_unit: str | None = None
    _weather_option_pressure_unit: str | None = None
    _weather_option_visibility_unit: str | None = None
    _weather_option_precipitation_unit: str | None = None
    _weather_option_wind_speed_unit: str | None = None

    def __post_init__(self, *args: Any, **kwargs: Any) -> None:
        """Finish initializing."""
        self._forecast_listeners = {"daily": [], "hourly": [], "twice_daily": []}

    async def async_internal_added_to_hass(self) -> None:
        """Call when the weather entity is added to hass."""
        await super().async_internal_added_to_hass()
        if not self.registry_entry:
            return
        self.async_registry_entry_updated()

    @cached_property
    def native_apparent_temperature(self) -> float | None:
        """Return the apparent temperature in native units."""
        return self._attr_native_apparent_temperature

    @cached_property
    def native_temperature(self) -> float | None:
        """Return the temperature in native units."""
        return self._attr_native_temperature

    @cached_property
    def native_temperature_unit(self) -> str | None:
        """Return the native unit of measurement for temperature."""
        return self._attr_native_temperature_unit

    @cached_property
    def native_dew_point(self) -> float | None:
        """Return the dew point temperature in native units."""
        return self._attr_native_dew_point

    @final
    @property
    def _default_temperature_unit(self) -> str:
        """Return the default unit of measurement for temperature.

        Should not be set by integrations.
        """
        return self.hass.config.units.temperature_unit

    @final
    @property
    def _temperature_unit(self) -> str:
        """Return the converted unit of measurement for temperature.

        Should not be set by integrations.
        """
        if (
            weather_option_temperature_unit := self._weather_option_temperature_unit
        ) is not None:
            return weather_option_temperature_unit

        return self._default_temperature_unit

    @cached_property
    def native_pressure(self) -> float | None:
        """Return the pressure in native units."""
        return self._attr_native_pressure

    @cached_property
    def native_pressure_unit(self) -> str | None:
        """Return the native unit of measurement for pressure."""
        return self._attr_native_pressure_unit

    @final
    @property
    def _default_pressure_unit(self) -> str:
        """Return the default unit of measurement for pressure.

        Should not be set by integrations.
        """
        if self.hass.config.units is US_CUSTOMARY_SYSTEM:
            return UnitOfPressure.INHG
        return UnitOfPressure.HPA

    @final
    @property
    def _pressure_unit(self) -> str:
        """Return the converted unit of measurement for pressure.

        Should not be set by integrations.
        """
        if (
            weather_option_pressure_unit := self._weather_option_pressure_unit
        ) is not None:
            return weather_option_pressure_unit

        return self._default_pressure_unit

    @cached_property
    def humidity(self) -> float | None:
        """Return the humidity in native units."""
        return self._attr_humidity

    @cached_property
    def native_wind_gust_speed(self) -> float | None:
        """Return the wind gust speed in native units."""
        return self._attr_native_wind_gust_speed

    @cached_property
    def native_wind_speed(self) -> float | None:
        """Return the wind speed in native units."""
        return self._attr_native_wind_speed

    @cached_property
    def native_wind_speed_unit(self) -> str | None:
        """Return the native unit of measurement for wind speed."""
        return self._attr_native_wind_speed_unit

    @final
    @property
    def _default_wind_speed_unit(self) -> str:
        """Return the default unit of measurement for wind speed.

        Should not be set by integrations.
        """
        if self.hass.config.units is US_CUSTOMARY_SYSTEM:
            return UnitOfSpeed.MILES_PER_HOUR
        return UnitOfSpeed.KILOMETERS_PER_HOUR

    @final
    @property
    def _wind_speed_unit(self) -> str:
        """Return the converted unit of measurement for wind speed.

        Should not be set by integrations.
        """
        if (
            weather_option_wind_speed_unit := self._weather_option_wind_speed_unit
        ) is not None:
            return weather_option_wind_speed_unit

        return self._default_wind_speed_unit

    @cached_property
    def wind_bearing(self) -> float | str | None:
        """Return the wind bearing."""
        return self._attr_wind_bearing

    @cached_property
    def ozone(self) -> float | None:
        """Return the ozone level."""
        return self._attr_ozone

    @cached_property
    def cloud_coverage(self) -> float | None:
        """Return the Cloud coverage in %."""
        return self._attr_cloud_coverage

    @cached_property
    def uv_index(self) -> float | None:
        """Return the UV index."""
        return self._attr_uv_index

    @cached_property
    def native_visibility(self) -> float | None:
        """Return the visibility in native units."""
        return self._attr_native_visibility

    @cached_property
    def native_visibility_unit(self) -> str | None:
        """Return the native unit of measurement for visibility."""
        return self._attr_native_visibility_unit

    @final
    @property
    def _default_visibility_unit(self) -> str:
        """Return the default unit of measurement for visibility.

        Should not be set by integrations.
        """
        return self.hass.config.units.length_unit

    @final
    @property
    def _visibility_unit(self) -> str:
        """Return the converted unit of measurement for visibility.

        Should not be set by integrations.
        """
        if (
            weather_option_visibility_unit := self._weather_option_visibility_unit
        ) is not None:
            return weather_option_visibility_unit

        return self._default_visibility_unit

    async def async_forecast_daily(self) -> list[Forecast] | None:
        """Return the daily forecast in native units."""
        raise NotImplementedError

    async def async_forecast_twice_daily(self) -> list[Forecast] | None:
        """Return the daily forecast in native units."""
        raise NotImplementedError

    async def async_forecast_hourly(self) -> list[Forecast] | None:
        """Return the hourly forecast in native units."""
        raise NotImplementedError

    @cached_property
    def native_precipitation_unit(self) -> str | None:
        """Return the native unit of measurement for accumulated precipitation."""
        return self._attr_native_precipitation_unit

    @final
    @property
    def _default_precipitation_unit(self) -> str:
        """Return the default unit of measurement for precipitation.

        Should not be set by integrations.
        """
        return self.hass.config.units.accumulated_precipitation_unit

    @final
    @property
    def _precipitation_unit(self) -> str:
        """Return the converted unit of measurement for precipitation.

        Should not be set by integrations.
        """
        if (
            weather_option_precipitation_unit := self._weather_option_precipitation_unit
        ) is not None:
            return weather_option_precipitation_unit

        return self._default_precipitation_unit

    @property
    def precision(self) -> float:
        """Return the precision of the temperature value, after unit conversion."""
        if hasattr(self, "_attr_precision"):
            return self._attr_precision
        return (
            PRECISION_TENTHS
            if self._temperature_unit == UnitOfTemperature.CELSIUS
            else PRECISION_WHOLE
        )

    @final
    @property
    def state_attributes(self) -> dict[str, Any]:
        """Return the state attributes, converted.

        Attributes are configured from native units to user-configured units.
        """
        data: dict[str, Any] = {}

        precision = self.precision

        if (temperature := self.native_temperature) is not None:
            from_unit = self.native_temperature_unit or self._default_temperature_unit
            to_unit = self._temperature_unit
            try:
                temperature_f = float(temperature)
                value_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT](
                    temperature_f, from_unit, to_unit
                )
                data[ATTR_WEATHER_TEMPERATURE] = round_temperature(
                    value_temp, precision
                )
            except (TypeError, ValueError):
                data[ATTR_WEATHER_TEMPERATURE] = temperature

        if (apparent_temperature := self.native_apparent_temperature) is not None:
            from_unit = self.native_temperature_unit or self._default_temperature_unit
            to_unit = self._temperature_unit
            try:
                apparent_temperature_f = float(apparent_temperature)
                value_apparent_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT](
                    apparent_temperature_f, from_unit, to_unit
                )
                data[ATTR_WEATHER_APPARENT_TEMPERATURE] = round_temperature(
                    value_apparent_temp, precision
                )
            except (TypeError, ValueError):
                data[ATTR_WEATHER_APPARENT_TEMPERATURE] = apparent_temperature

        if (dew_point := self.native_dew_point) is not None:
            from_unit = self.native_temperature_unit or self._default_temperature_unit
            to_unit = self._temperature_unit
            try:
                dew_point_f = float(dew_point)
                value_dew_point = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT](
                    dew_point_f, from_unit, to_unit
                )
                data[ATTR_WEATHER_DEW_POINT] = round_temperature(
                    value_dew_point, precision
                )
            except (TypeError, ValueError):
                data[ATTR_WEATHER_DEW_POINT] = dew_point

        data[ATTR_WEATHER_TEMPERATURE_UNIT] = self._temperature_unit

        if (humidity := self.humidity) is not None:
            data[ATTR_WEATHER_HUMIDITY] = round(humidity)

        if (ozone := self.ozone) is not None:
            data[ATTR_WEATHER_OZONE] = ozone

        if (cloud_coverage := self.cloud_coverage) is not None:
            data[ATTR_WEATHER_CLOUD_COVERAGE] = cloud_coverage

        if (uv_index := self.uv_index) is not None:
            data[ATTR_WEATHER_UV_INDEX] = uv_index

        if (pressure := self.native_pressure) is not None:
            from_unit = self.native_pressure_unit or self._default_pressure_unit
            to_unit = self._pressure_unit
            try:
                pressure_f = float(pressure)
                value_pressure = UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT](
                    pressure_f, from_unit, to_unit
                )
                data[ATTR_WEATHER_PRESSURE] = round(value_pressure, ROUNDING_PRECISION)
            except (TypeError, ValueError):
                data[ATTR_WEATHER_PRESSURE] = pressure

        data[ATTR_WEATHER_PRESSURE_UNIT] = self._pressure_unit

        if (wind_bearing := self.wind_bearing) is not None:
            data[ATTR_WEATHER_WIND_BEARING] = wind_bearing

        if (wind_gust_speed := self.native_wind_gust_speed) is not None:
            from_unit = self.native_wind_speed_unit or self._default_wind_speed_unit
            to_unit = self._wind_speed_unit
            try:
                wind_gust_speed_f = float(wind_gust_speed)
                value_wind_gust_speed = UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT](
                    wind_gust_speed_f, from_unit, to_unit
                )
                data[ATTR_WEATHER_WIND_GUST_SPEED] = round(
                    value_wind_gust_speed, ROUNDING_PRECISION
                )
            except (TypeError, ValueError):
                data[ATTR_WEATHER_WIND_GUST_SPEED] = wind_gust_speed

        if (wind_speed := self.native_wind_speed) is not None:
            from_unit = self.native_wind_speed_unit or self._default_wind_speed_unit
            to_unit = self._wind_speed_unit
            try:
                wind_speed_f = float(wind_speed)
                value_wind_speed = UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT](
                    wind_speed_f, from_unit, to_unit
                )
                data[ATTR_WEATHER_WIND_SPEED] = round(
                    value_wind_speed, ROUNDING_PRECISION
                )
            except (TypeError, ValueError):
                data[ATTR_WEATHER_WIND_SPEED] = wind_speed

        data[ATTR_WEATHER_WIND_SPEED_UNIT] = self._wind_speed_unit

        if (visibility := self.native_visibility) is not None:
            from_unit = self.native_visibility_unit or self._default_visibility_unit
            to_unit = self._visibility_unit
            try:
                visibility_f = float(visibility)
                value_visibility = UNIT_CONVERSIONS[ATTR_WEATHER_VISIBILITY_UNIT](
                    visibility_f, from_unit, to_unit
                )
                data[ATTR_WEATHER_VISIBILITY] = round(
                    value_visibility, ROUNDING_PRECISION
                )
            except (TypeError, ValueError):
                data[ATTR_WEATHER_VISIBILITY] = visibility

        data[ATTR_WEATHER_VISIBILITY_UNIT] = self._visibility_unit
        data[ATTR_WEATHER_PRECIPITATION_UNIT] = self._precipitation_unit

        return data

    @final
    def _convert_forecast(
        self, native_forecast_list: list[Forecast]
    ) -> list[JsonValueType]:
        """Convert a forecast in native units to the unit configured by the user."""
        converted_forecast_list: list[JsonValueType] = []
        precision = self.precision

        from_temp_unit = self.native_temperature_unit or self._default_temperature_unit
        to_temp_unit = self._temperature_unit

        for _forecast_entry in native_forecast_list:
            forecast_entry: dict[str, Any] = dict(_forecast_entry)

            temperature = forecast_entry.pop(
                ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP)
            )

            if temperature is None:
                forecast_entry[ATTR_FORECAST_TEMP] = None
            else:
                with suppress(TypeError, ValueError):
                    temperature_f = float(temperature)
                    value_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT](
                        temperature_f,
                        from_temp_unit,
                        to_temp_unit,
                    )
                    forecast_entry[ATTR_FORECAST_TEMP] = round_temperature(
                        value_temp, precision
                    )

            if (
                forecast_apparent_temp := forecast_entry.pop(
                    ATTR_FORECAST_NATIVE_APPARENT_TEMP,
                    forecast_entry.get(ATTR_FORECAST_NATIVE_APPARENT_TEMP),
                )
            ) is not None:
                with suppress(TypeError, ValueError):
                    forecast_apparent_temp = float(forecast_apparent_temp)
                    value_apparent_temp = UNIT_CONVERSIONS[
                        ATTR_WEATHER_TEMPERATURE_UNIT
                    ](
                        forecast_apparent_temp,
                        from_temp_unit,
                        to_temp_unit,
                    )

                    forecast_entry[ATTR_FORECAST_APPARENT_TEMP] = round_temperature(
                        value_apparent_temp, precision
                    )

            if (
                forecast_temp_low := forecast_entry.pop(
                    ATTR_FORECAST_NATIVE_TEMP_LOW,
                    forecast_entry.get(ATTR_FORECAST_TEMP_LOW),
                )
            ) is not None:
                with suppress(TypeError, ValueError):
                    forecast_temp_low_f = float(forecast_temp_low)
                    value_temp_low = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT](
                        forecast_temp_low_f,
                        from_temp_unit,
                        to_temp_unit,
                    )

                    forecast_entry[ATTR_FORECAST_TEMP_LOW] = round_temperature(
                        value_temp_low, precision
                    )

            if (
                forecast_dew_point := forecast_entry.pop(
                    ATTR_FORECAST_NATIVE_DEW_POINT,
                    None,
                )
            ) is not None:
                with suppress(TypeError, ValueError):
                    forecast_dew_point_f = float(forecast_dew_point)
                    value_dew_point = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT](
                        forecast_dew_point_f,
                        from_temp_unit,
                        to_temp_unit,
                    )

                    forecast_entry[ATTR_FORECAST_DEW_POINT] = round_temperature(
                        value_dew_point, precision
                    )

            if (
                forecast_pressure := forecast_entry.pop(
                    ATTR_FORECAST_NATIVE_PRESSURE,
                    forecast_entry.get(ATTR_FORECAST_PRESSURE),
                )
            ) is not None:
                from_pressure_unit = (
                    self.native_pressure_unit or self._default_pressure_unit
                )
                to_pressure_unit = self._pressure_unit
                with suppress(TypeError, ValueError):
                    forecast_pressure_f = float(forecast_pressure)
                    forecast_entry[ATTR_FORECAST_PRESSURE] = round(
                        UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT](
                            forecast_pressure_f,
                            from_pressure_unit,
                            to_pressure_unit,
                        ),
                        ROUNDING_PRECISION,
                    )

            if (
                forecast_wind_gust_speed := forecast_entry.pop(
                    ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
                    None,
                )
            ) is not None:
                from_wind_speed_unit = (
                    self.native_wind_speed_unit or self._default_wind_speed_unit
                )
                to_wind_speed_unit = self._wind_speed_unit
                with suppress(TypeError, ValueError):
                    forecast_wind_gust_speed_f = float(forecast_wind_gust_speed)
                    forecast_entry[ATTR_FORECAST_WIND_GUST_SPEED] = round(
                        UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT](
                            forecast_wind_gust_speed_f,
                            from_wind_speed_unit,
                            to_wind_speed_unit,
                        ),
                        ROUNDING_PRECISION,
                    )

            if (
                forecast_wind_speed := forecast_entry.pop(
                    ATTR_FORECAST_NATIVE_WIND_SPEED,
                    forecast_entry.get(ATTR_FORECAST_WIND_SPEED),
                )
            ) is not None:
                from_wind_speed_unit = (
                    self.native_wind_speed_unit or self._default_wind_speed_unit
                )
                to_wind_speed_unit = self._wind_speed_unit
                with suppress(TypeError, ValueError):
                    forecast_wind_speed_f = float(forecast_wind_speed)
                    forecast_entry[ATTR_FORECAST_WIND_SPEED] = round(
                        UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT](
                            forecast_wind_speed_f,
                            from_wind_speed_unit,
                            to_wind_speed_unit,
                        ),
                        ROUNDING_PRECISION,
                    )

            if (
                forecast_precipitation := forecast_entry.pop(
                    ATTR_FORECAST_NATIVE_PRECIPITATION,
                    forecast_entry.get(ATTR_FORECAST_PRECIPITATION),
                )
            ) is not None:
                from_precipitation_unit = (
                    self.native_precipitation_unit or self._default_precipitation_unit
                )
                to_precipitation_unit = self._precipitation_unit
                with suppress(TypeError, ValueError):
                    forecast_precipitation_f = float(forecast_precipitation)
                    forecast_entry[ATTR_FORECAST_PRECIPITATION] = round(
                        UNIT_CONVERSIONS[ATTR_WEATHER_PRECIPITATION_UNIT](
                            forecast_precipitation_f,
                            from_precipitation_unit,
                            to_precipitation_unit,
                        ),
                        ROUNDING_PRECISION,
                    )

            if (
                forecast_humidity := forecast_entry.pop(
                    ATTR_FORECAST_HUMIDITY,
                    None,
                )
            ) is not None:
                with suppress(TypeError, ValueError):
                    forecast_humidity_f = float(forecast_humidity)
                    forecast_entry[ATTR_FORECAST_HUMIDITY] = round(forecast_humidity_f)

            converted_forecast_list.append(forecast_entry)

        return converted_forecast_list

    @property
    @final
    def state(self) -> str | None:
        """Return the current state."""
        return self.condition

    @cached_property
    def condition(self) -> str | None:
        """Return the current condition."""
        return self._attr_condition

    @callback
    def async_registry_entry_updated(self) -> None:
        """Run when the entity registry entry has been updated."""
        assert self.registry_entry
        self._weather_option_temperature_unit = None
        self._weather_option_pressure_unit = None
        self._weather_option_precipitation_unit = None
        self._weather_option_wind_speed_unit = None
        self._weather_option_visibility_unit = None
        if weather_options := self.registry_entry.options.get(DOMAIN):
            if (
                custom_unit_temperature := weather_options.get(
                    ATTR_WEATHER_TEMPERATURE_UNIT
                )
            ) and custom_unit_temperature in VALID_UNITS[ATTR_WEATHER_TEMPERATURE_UNIT]:
                self._weather_option_temperature_unit = custom_unit_temperature
            if (
                custom_unit_pressure := weather_options.get(ATTR_WEATHER_PRESSURE_UNIT)
            ) and custom_unit_pressure in VALID_UNITS[ATTR_WEATHER_PRESSURE_UNIT]:
                self._weather_option_pressure_unit = custom_unit_pressure
            if (
                custom_unit_precipitation := weather_options.get(
                    ATTR_WEATHER_PRECIPITATION_UNIT
                )
            ) and custom_unit_precipitation in VALID_UNITS[
                ATTR_WEATHER_PRECIPITATION_UNIT
            ]:
                self._weather_option_precipitation_unit = custom_unit_precipitation
            if (
                custom_unit_wind_speed := weather_options.get(
                    ATTR_WEATHER_WIND_SPEED_UNIT
                )
            ) and custom_unit_wind_speed in VALID_UNITS[ATTR_WEATHER_WIND_SPEED_UNIT]:
                self._weather_option_wind_speed_unit = custom_unit_wind_speed
            if (
                custom_unit_visibility := weather_options.get(
                    ATTR_WEATHER_VISIBILITY_UNIT
                )
            ) and custom_unit_visibility in VALID_UNITS[ATTR_WEATHER_VISIBILITY_UNIT]:
                self._weather_option_visibility_unit = custom_unit_visibility

    @callback
    def _async_subscription_started(
        self,
        forecast_type: Literal["daily", "hourly", "twice_daily"],
    ) -> None:
        """Start subscription to forecast_type."""

    @callback
    def _async_subscription_ended(
        self,
        forecast_type: Literal["daily", "hourly", "twice_daily"],
    ) -> None:
        """End subscription to forecast_type."""

    @final
    @callback
    def async_subscribe_forecast(
        self,
        forecast_type: Literal["daily", "hourly", "twice_daily"],
        forecast_listener: Callable[[list[JsonValueType] | None], None],
    ) -> CALLBACK_TYPE:
        """Subscribe to forecast updates.

        Called by websocket API.
        """
        subscription_started = not self._forecast_listeners[forecast_type]
        self._forecast_listeners[forecast_type].append(forecast_listener)
        if subscription_started:
            self._async_subscription_started(forecast_type)

        @callback
        def unsubscribe() -> None:
            self._forecast_listeners[forecast_type].remove(forecast_listener)
            if not self._forecast_listeners[forecast_type]:
                self._async_subscription_ended(forecast_type)

        return unsubscribe

    @final
    async def async_update_listeners(
        self, forecast_types: Iterable[Literal["daily", "hourly", "twice_daily"]] | None
    ) -> None:
        """Push updated forecast to all listeners."""
        if forecast_types is None:
            forecast_types = {"daily", "hourly", "twice_daily"}
        for forecast_type in forecast_types:
            if not self._forecast_listeners[forecast_type]:
                continue

            native_forecast_list: list[Forecast] | None = await getattr(
                self, f"async_forecast_{forecast_type}"
            )()

            if native_forecast_list is None:
                for listener in self._forecast_listeners[forecast_type]:
                    listener(None)
                continue

            if forecast_type == "twice_daily":
                for fc_twice_daily in native_forecast_list:
                    if fc_twice_daily.get(ATTR_FORECAST_IS_DAYTIME) is None:
                        raise ValueError(
                            "is_daytime mandatory attribute for forecast_twice_daily is missing"
                        )

            converted_forecast_list = self._convert_forecast(native_forecast_list)
            for listener in self._forecast_listeners[forecast_type]:
                listener(converted_forecast_list)


def raise_unsupported_forecast(entity_id: str, forecast_type: str) -> None:
    """Raise error on attempt to get an unsupported forecast."""
    raise HomeAssistantError(
        f"Weather entity '{entity_id}' does not support '{forecast_type}' forecast"
    )


async def async_get_forecasts_service(
    weather: WeatherEntity, service_call: ServiceCall
) -> ServiceResponse:
    """Get weather forecast."""
    forecast_type = service_call.data["type"]
    supported_features = weather.supported_features or 0
    if forecast_type == "daily":
        if (supported_features & WeatherEntityFeature.FORECAST_DAILY) == 0:
            raise_unsupported_forecast(weather.entity_id, forecast_type)
        native_forecast_list = await weather.async_forecast_daily()
    elif forecast_type == "hourly":
        if (supported_features & WeatherEntityFeature.FORECAST_HOURLY) == 0:
            raise_unsupported_forecast(weather.entity_id, forecast_type)
        native_forecast_list = await weather.async_forecast_hourly()
    else:
        if (supported_features & WeatherEntityFeature.FORECAST_TWICE_DAILY) == 0:
            raise_unsupported_forecast(weather.entity_id, forecast_type)
        native_forecast_list = await weather.async_forecast_twice_daily()
    if native_forecast_list is None:
        converted_forecast_list = []
    else:
        converted_forecast_list = weather._convert_forecast(native_forecast_list)  # noqa: SLF001
    return {
        "forecast": converted_forecast_list,
    }


class CoordinatorWeatherEntity(
    CoordinatorEntity[_ObservationUpdateCoordinatorT],
    WeatherEntity,
    Generic[
        _ObservationUpdateCoordinatorT,
        _DailyForecastUpdateCoordinatorT,
        _HourlyForecastUpdateCoordinatorT,
        _TwiceDailyForecastUpdateCoordinatorT,
    ],
):
    """A class for weather entities using DataUpdateCoordinators."""

    def __init__(
        self,
        observation_coordinator: _ObservationUpdateCoordinatorT,
        *,
        context: Any = None,
        daily_coordinator: _DailyForecastUpdateCoordinatorT | None = None,
        hourly_coordinator: _HourlyForecastUpdateCoordinatorT | None = None,
        twice_daily_coordinator: _TwiceDailyForecastUpdateCoordinatorT | None = None,
        daily_forecast_valid: timedelta | None = None,
        hourly_forecast_valid: timedelta | None = None,
        twice_daily_forecast_valid: timedelta | None = None,
    ) -> None:
        """Initialize."""
        super().__init__(observation_coordinator, context)
        self.forecast_coordinators = {
            "daily": daily_coordinator,
            "hourly": hourly_coordinator,
            "twice_daily": twice_daily_coordinator,
        }
        self.forecast_valid = {
            "daily": daily_forecast_valid,
            "hourly": hourly_forecast_valid,
            "twice_daily": twice_daily_forecast_valid,
        }
        self.unsub_forecast: dict[str, Callable[[], None] | None] = {
            "daily": None,
            "hourly": None,
            "twice_daily": None,
        }

    async def async_added_to_hass(self) -> None:
        """When entity is added to hass."""
        await super().async_added_to_hass()
        self.async_on_remove(partial(self._remove_forecast_listener, "daily"))
        self.async_on_remove(partial(self._remove_forecast_listener, "hourly"))
        self.async_on_remove(partial(self._remove_forecast_listener, "twice_daily"))

    def _remove_forecast_listener(
        self, forecast_type: Literal["daily", "hourly", "twice_daily"]
    ) -> None:
        """Remove weather forecast listener."""
        if unsub_fn := self.unsub_forecast[forecast_type]:
            unsub_fn()
            self.unsub_forecast[forecast_type] = None

    @callback
    def _async_subscription_started(
        self,
        forecast_type: Literal["daily", "hourly", "twice_daily"],
    ) -> None:
        """Start subscription to forecast_type."""
        if not (coordinator := self.forecast_coordinators[forecast_type]):
            return
        self.unsub_forecast[forecast_type] = coordinator.async_add_listener(
            partial(self._handle_forecast_update, forecast_type)
        )

    @callback
    def _handle_daily_forecast_coordinator_update(self) -> None:
        """Handle updated data from the daily forecast coordinator."""

    @callback
    def _handle_hourly_forecast_coordinator_update(self) -> None:
        """Handle updated data from the hourly forecast coordinator."""

    @callback
    def _handle_twice_daily_forecast_coordinator_update(self) -> None:
        """Handle updated data from the twice daily forecast coordinator."""

    @final
    @callback
    def _handle_forecast_update(
        self, forecast_type: Literal["daily", "hourly", "twice_daily"]
    ) -> None:
        """Update forecast data."""
        coordinator = self.forecast_coordinators[forecast_type]
        assert coordinator
        assert coordinator.config_entry is not None
        getattr(self, f"_handle_{forecast_type}_forecast_coordinator_update")()
        coordinator.config_entry.async_create_task(
            self.hass, self.async_update_listeners((forecast_type,))
        )

    @callback
    def _async_subscription_ended(
        self,
        forecast_type: Literal["daily", "hourly", "twice_daily"],
    ) -> None:
        """End subscription to forecast_type."""
        self._remove_forecast_listener(forecast_type)

    @final
    async def _async_refresh_forecast(
        self,
        coordinator: TimestampDataUpdateCoordinator[Any],
        forecast_valid_time: timedelta | None,
    ) -> bool:
        """Refresh stale forecast if needed."""
        if coordinator.update_interval is None:
            return True
        if forecast_valid_time is None:
            forecast_valid_time = coordinator.update_interval
        if (
            not (last_success_time := coordinator.last_update_success_time)
            or utcnow() - last_success_time >= coordinator.update_interval
        ):
            await coordinator.async_refresh()
        if (
            not (last_success_time := coordinator.last_update_success_time)
            or utcnow() - last_success_time >= forecast_valid_time
        ):
            return False
        return True

    @callback
    def _async_forecast_daily(self) -> list[Forecast] | None:
        """Return the daily forecast in native units."""
        raise NotImplementedError

    @callback
    def _async_forecast_hourly(self) -> list[Forecast] | None:
        """Return the hourly forecast in native units."""
        raise NotImplementedError

    @callback
    def _async_forecast_twice_daily(self) -> list[Forecast] | None:
        """Return the twice daily forecast in native units."""
        raise NotImplementedError

    @final
    async def _async_forecast(
        self, forecast_type: Literal["daily", "hourly", "twice_daily"]
    ) -> list[Forecast] | None:
        """Return the forecast in native units."""
        coordinator = self.forecast_coordinators[forecast_type]
        if coordinator and not await self._async_refresh_forecast(
            coordinator, self.forecast_valid[forecast_type]
        ):
            return None
        return cast(
            list[Forecast] | None, getattr(self, f"_async_forecast_{forecast_type}")()
        )

    @final
    async def async_forecast_daily(self) -> list[Forecast] | None:
        """Return the daily forecast in native units."""
        return await self._async_forecast("daily")

    @final
    async def async_forecast_hourly(self) -> list[Forecast] | None:
        """Return the hourly forecast in native units."""
        return await self._async_forecast("hourly")

    @final
    async def async_forecast_twice_daily(self) -> list[Forecast] | None:
        """Return the twice daily forecast in native units."""
        return await self._async_forecast("twice_daily")


class SingleCoordinatorWeatherEntity(
    CoordinatorWeatherEntity[
        _ObservationUpdateCoordinatorT, TimestampDataUpdateCoordinator[None]
    ],
):
    """A class for weather entities using a single DataUpdateCoordinators.

    This class is added as a convenience.
    """

    def __init__(
        self,
        coordinator: _ObservationUpdateCoordinatorT,
        context: Any = None,
    ) -> None:
        """Initialize."""
        super().__init__(coordinator, context=context)

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        super()._handle_coordinator_update()
        assert self.coordinator.config_entry
        self.coordinator.config_entry.async_create_task(
            self.hass, self.async_update_listeners(None)
        )
